// This file is part of Notepad4.
// See License.txt for details about distribution and modification.
//! Lexer for PHP

#include <cassert>
#include <cstring>

#include <string>
#include <string_view>
#include <vector>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "StringUtils.h"
#include "LexerModule.h"
#include "LexerUtils.h"
#include "DocUtils.h"

using namespace Scintilla;
using namespace Lexilla;

namespace {

enum class HtmlTagType {
	None,
	Question,	// <?xml ?>
	Normal,
	Void,		// void tag
	Script,
	Style,
};

enum class DocTagState {
	None,
	At,				// @param x
	InlineAt,		// {@link uri}
};

enum {
	LineStateCommentLine = 1 << 0,
	LineStateUseNamespace = 1 << 1,
	LineStateNestedStateLine = 1 << 2,
	LineStateAttributeLine = 1 << 3,
	JsLineStateLineContinuation = 1 << 4,
	CssLineStatePropertyValue = 1 << 5,
};

//KeywordIndex++Autogenerated -- start of section automatically generated
enum {
	KeywordIndex_Keyword = 0,
	KeywordIndex_Type = 1,
	KeywordIndex_Class = 2,
	KeywordIndex_Interface = 3,
	KeywordIndex_PredefinedVariable = 4,
	KeywordIndex_MagicConstant = 5,
	KeywordIndex_MagicMethod = 6,
	KeywordIndex_JavaScript = 10,
};
//KeywordIndex--Autogenerated -- end of section automatically generated

enum class KeywordType {
	None = 0,
	Const = SCE_PHP_WORD2,
	Class = SCE_PHP_CLASS,
	Interface = SCE_PHP_INTERFACE,
	Trait = SCE_PHP_TRAIT,
	Enum = SCE_PHP_ENUM,
	Function = SCE_PHP_FUNCTION_DEFINITION,
	Constant = SCE_PHP_CONSTANT,
	Label = SCE_PHP_LABEL,
};

enum class HtmlTextBlock {
	Html,
	PHP,
	Script,
	Style,
};

constexpr HtmlTextBlock GetHtmlTextBlock(int style) noexcept {
	if (style >= css_style(SCE_CSS_DEFAULT)) {
		return HtmlTextBlock::Style;
	}
	if (style >= js_style(SCE_JS_DEFAULT)) {
		return HtmlTextBlock::Script;
	}
	if (style >= SCE_PHP_DEFAULT) {
		return HtmlTextBlock::PHP;
	}
	return HtmlTextBlock::Html;
}

constexpr bool StyleNeedsBacktrack(int state) noexcept {
	return state == SCE_PHP_HEREDOC || state == SCE_PHP_NOWDOC;
}

constexpr uint8_t GetPHPStringQuote(int state) noexcept {
	return (state == SCE_PHP_STRING_SQ) ? '\''
		: ((state == SCE_PHP_STRING_DQ) ? '\"' : '`');
}

enum class VariableType {
	Normal,
	Simple,		// $variable
	Complex,	// ${variable}, {$variable}
};

struct VariableExpansion {
	VariableType type;
	int braceCount;
};

constexpr bool ExpandVariable(int state) noexcept {
	return state == SCE_PHP_STRING_DQ || state == SCE_PHP_HEREDOC || state == SCE_PHP_STRING_BT;
}

// https://www.php.net/manual/en/language.types.string.php
struct EscapeSequence {
	int outerState = SCE_PHP_DEFAULT;
	int digitsLeft = 0;
	bool hex = false;
	bool brace = false;

	// highlight any character as escape sequence.
	void resetEscapeState(int state, int chNext) noexcept {
		outerState = state;
		digitsLeft = 1;
		hex = true;
		brace = false;
		if (chNext == 'x') {
			digitsLeft = 3;
		} else if (IsOctalDigit(chNext) && state < SCE_PHP_LABEL) {
			digitsLeft = 3;
			hex = false;
		} else if (chNext == 'u') {
			digitsLeft = 5;
		}
	}
	bool atEscapeEnd(int ch) noexcept {
		--digitsLeft;
		return digitsLeft <= 0 || !IsOctalOrHex(ch, hex);
	}
	bool atUnicodeRangeEnd(int ch) noexcept {
		--digitsLeft;
		return digitsLeft <= 0 || !IsCssUnicodeRangeChar(ch);
	}
};

struct PHPLexer {
	StyleContext sc;
	std::vector<int> nestedState;
	std::vector<VariableExpansion> nestedExpansion;
	std::string hereDocId;

	HtmlTagType tagType = HtmlTagType::None;
	KeywordType kwType = KeywordType::None;
	EscapeSequence escSeq;
	bool insideUrl = false;
	int lineStateLineType = 0;
	int lineStateAttribute = 0;
	int lineContinuation = 0;
	int propertyValue = 0;
	int parenCount = 0;
	int selectorLevel = 0;	// nested selector
	int chBefore = 0;

	PHPLexer(Sci_PositionU startPos, Sci_PositionU lengthDoc, int initStyle, Accessor &styler) noexcept:
		sc(startPos, lengthDoc, initStyle, styler) {}

	void SaveOuterStyle(int style) {
		nestedState.push_back(style);
	}
	int TakeOuterStyle() {
		return TakeAndPop(nestedState);
	}
	int TryTakeOuterStyle() {
		return TryTakeAndPop(nestedState);
	}
	void EnterExpansion(VariableType type, int braceCount) {
		nestedExpansion.push_back({type, braceCount});
	}
	void ExitExpansion() {
		nestedExpansion.pop_back();
	}
	VariableType GetVariableType() const noexcept {
		return nestedExpansion.empty() ? VariableType::Normal : nestedExpansion.back().type;
	}

	int LineState() const noexcept {
		int lineState = lineStateLineType | lineStateAttribute | lineContinuation
			| propertyValue | (parenCount << 8) | (selectorLevel << 16);
		if (tagType == HtmlTagType::Question || StyleNeedsBacktrack(sc.state) || !nestedState.empty()) {
			lineState |= LineStateNestedStateLine;
		}
		return lineState;
	}
	bool IsHereDocEnd() const noexcept {
		return sc.Match(hereDocId.c_str()) && !IsIdentifierCharEx(sc.GetRelative(hereDocId.length()));
	}

	void ClassifyHtmlTag();
	bool HandleBlockEnd(HtmlTextBlock block);

	void HandlePHPTag();
	bool ClassifyPHPWord(LexerWordList keywordLists, int visibleChars);
	bool HighlightInnerString();
	bool HighlightOperator(HtmlTextBlock block, int stylePrevNonWhite);

	int ClassifyJSWord(LexerWordList keywordLists);
	void HighlightJsInnerString();
	bool ClassifyCssWord();
};

void PHPLexer::ClassifyHtmlTag() {
	if (sc.state == SCE_H_OTHER) {
		sc.SetState((tagType == HtmlTagType::Question) ? SCE_H_QUESTION : SCE_H_TAG);
	} else if (tagType == HtmlTagType::None) {
		char s[16]{};
		sc.GetCurrentLowered(s, sizeof(s) - 1);
		char *p = s + 1;
		if (StrEqual(p, "script")) {
			tagType = HtmlTagType::Script;
		} else if (StrEqual(p, "style")) {
			tagType = HtmlTagType::Style;
		} else {
			tagType = HtmlTagType::Normal;
			const size_t length = sc.LengthCurrent();
			if (length <= maxHtmlVoidTagLen + 2) {
				s[length] = ' ';
				if (*p != '/') {
					--p;
				}
				*p = ' ';
				if (nullptr != strstr(htmlVoidTagList, p)) {
					tagType = HtmlTagType::Void;
				}
			}
		}
	}

	int state = SCE_H_OTHER;
	if (tagType == HtmlTagType::Void) {
		sc.ChangeState(SCE_H_VOID_TAG);
	} else if (tagType > HtmlTagType::Void && sc.Match('/', '>')) {
		tagType = HtmlTagType::Normal;
	}
	if (sc.ch > ' ') {
		state = SCE_H_DEFAULT;
		if (tagType == HtmlTagType::Script) {
			state = js_style(SCE_JS_DEFAULT);
		} else if (tagType == HtmlTagType::Style) {
			state = css_style(SCE_CSS_DEFAULT);
		}
		tagType = HtmlTagType::None;
		sc.Forward((sc.ch == '>') ? 1 : 2);
	}
	sc.SetState(state);
	if (sc.Match('<', '?')) {
		HandlePHPTag();
	}
}

bool PHPLexer::HandleBlockEnd(HtmlTextBlock block) {
	if (block == HtmlTextBlock::PHP) {
		kwType = KeywordType::None;
		const int outer = TryTakeOuterStyle();
		lineStateLineType = nestedState.empty() ? 0 : LineStateNestedStateLine;
		nestedState.clear();
		nestedExpansion.clear();
		sc.SetState(SCE_H_QUESTION);
		sc.Forward();
		sc.ForwardSetState(outer);
		return true;
	}

	const char *tag = (block == HtmlTextBlock::Script) ? "script" : "style";
	if (sc.styler.MatchLowerCase(sc.currentPos + 2, tag)) {
		kwType = KeywordType::None;
		tagType = HtmlTagType::None;
		lineStateAttribute = 0;
		propertyValue = 0;
		parenCount = 0;
		selectorLevel = 0;
		lineStateLineType = nestedState.empty() ? 0 : LineStateNestedStateLine;
		nestedState.clear();
		sc.SetState(SCE_H_TAG);
		sc.Forward();
		return true;
	}
	return false;
}

void PHPLexer::HandlePHPTag() {
// we only support standard tag <?php ?>, and short echo tag <?= ?>
// short open tag <? ?> was deprecated since PHP 7.4 and removed in PHP 8
// see https://wiki.php.net/rfc/deprecate_php_short_tags
// ASP tag <% %>, <%= %> and script tag <script language="php"></script> were removed in PHP 7
// see https://wiki.php.net/rfc/remove_alternative_php_tags

	int offset = 0;
	const int chNext = sc.GetRelative(2);
	if (chNext == '=') {
		offset = 2;
	} else if (chNext == 'p' || (sc.state == SCE_H_DEFAULT && IsHtmlTagStart(chNext))) {
		if (chNext == 'p' && sc.GetRelative(3) == 'h' && sc.GetRelative(4) == 'p' && IsASpace(sc.GetRelative(5))) {
			offset = 5;
		} else if (sc.state == SCE_H_DEFAULT) {
			tagType = HtmlTagType::Question;
			sc.SetState(SCE_H_QUESTION);
		}
	}
	if (offset != 0) {
		const int outer = sc.state;
		sc.SetState(SCE_H_QUESTION);
		sc.Advance(offset);
		sc.SetState(SCE_PHP_DEFAULT);
		if (outer != SCE_H_DEFAULT) {
			SaveOuterStyle(outer);
		}
	}
}

bool PHPLexer::ClassifyPHPWord(LexerWordList keywordLists, int visibleChars) {
	if (sc.state == SCE_PHP_HEREDOC_ID || sc.state == SCE_PHP_NOWDOC_ID) {
		hereDocId = sc.styler.GetRange(sc.styler.GetStartSegment(), sc.currentPos);
		insideUrl = false;
		if (sc.state == SCE_PHP_HEREDOC_ID) {
			if (sc.ch == '\"') {
				sc.Forward();
			}
			sc.SetState(SCE_PHP_HEREDOC);
		} else {
			if (sc.ch == '\'') {
				sc.Forward();
			}
			sc.SetState(SCE_PHP_NOWDOC);
		}
		return false;
	}
	const VariableType variableType = GetVariableType();
	if (variableType == VariableType::Simple && nestedExpansion.back().braceCount == 0) {
		// avoid highlighting object property to simplify code folding
		if (sc.state != SCE_PHP_VARIABLE2) {
			sc.ChangeState(SCE_PHP_IDENTIFIER2);
		}
		if (sc.ch == '[') {
			nestedExpansion.back().braceCount = 1;
		} else if (!sc.Match('-', '>')) {
			kwType = KeywordType::None;
			ExitExpansion();
			sc.SetState(TakeOuterStyle());
			return true;
		}
	} else {
		char s[128];
		sc.GetCurrent(s, sizeof(s));
		// variable, constant and enum name is case-sensitive
		if (sc.state == SCE_PHP_VARIABLE) {
			if (keywordLists[KeywordIndex_PredefinedVariable].InList(s + 1)) {
				sc.ChangeState(SCE_PHP_PREDEFINED_VARIABLE);
			}
		} else if (keywordLists[KeywordIndex_MagicConstant].InList(s)) {
			sc.ChangeState(SCE_PHP_MAGIC_CONSTANT);
		} else {
			char origin[sizeof(s)];
			memcpy(origin, s, sizeof(s));
			ToLowerCase(s);
			if (keywordLists[KeywordIndex_Keyword].InListPrefixed(s, '(')) {
				sc.ChangeState(SCE_PHP_WORD);
				if (visibleChars == 3 && StrEqual(s, "use")) {
					lineStateLineType = LineStateUseNamespace;
				} else if (StrEqualsAny(s, "class", "new", "extends", "instanceof")) {
					kwType = KeywordType::Class;
				} else if (StrEqualsAny(s, "interface", "implements")) {
					kwType = KeywordType::Interface;
				} else if (StrEqual(s, "enum")) {
					kwType = KeywordType::Enum;
				} else if (StrEqual(s, "const")) {
					kwType = KeywordType::Const;
				} else if (StrEqual(s, "trait")) {
					kwType = KeywordType::Trait;
				} else if (StrEqual(s, "function")) {
					kwType = KeywordType::Function;
				} else if (StrEqual(s, "goto")) {
					kwType = KeywordType::Label;
				}
				if (kwType != KeywordType::None) {
					const int chNext = sc.GetLineNextChar();
					if (!IsIdentifierStartEx(chNext)) {
						kwType = KeywordType::None;
					}
				}
			} else if (keywordLists[KeywordIndex_Type].InList(s)) {
				sc.ChangeState(SCE_PHP_WORD2);
			} else if (keywordLists[KeywordIndex_Interface].InList(s)) {
				sc.ChangeState(SCE_PHP_INTERFACE);
			} else if (sc.Match(':', ':') || keywordLists[KeywordIndex_Class].InList(s)) {
				// Name::class
				sc.ChangeState(SCE_PHP_CLASS);
			} else {
				const int chNext = sc.GetDocNextChar();
				if (lineStateAttribute && (chNext == '(' || chNext == ']')) {
					sc.ChangeState(SCE_PHP_ATTRIBUTE);
				} else if (chNext == '(') {
					if (keywordLists[KeywordIndex_MagicMethod].InListPrefixed(s, '(')) {
						sc.ChangeState(SCE_PHP_MAGIC_METHOD);
					} else {
						sc.ChangeState((kwType == KeywordType::Function) ? static_cast<int>(kwType) : SCE_PHP_FUNCTION);
					}
				} else if (chNext == '$') {
					// type $variable
					sc.ChangeState(SCE_PHP_CLASS);
				} else if (lineStateAttribute == 0 && sc.ch == ':' && IsJumpLabelPrevChar(chBefore)) {
					sc.ChangeState(SCE_PHP_LABEL);
				} else if (kwType != KeywordType::None) {
					if (kwType == KeywordType::Const) {
						// const [type] name = value;
						kwType = KeywordType::None;
						if (chNext == '=') {
							sc.ChangeState(SCE_PHP_CONSTANT);
						} else if (IsIdentifierStartEx(chNext)) {
							kwType = KeywordType::Constant;
						}
					} else {
						sc.ChangeState(static_cast<int>(kwType));
						kwType = KeywordType::None;
					}
				} else if (IsUpperOrNumeric(origin)) {
					sc.ChangeState(SCE_PHP_CONSTANT);
				}
			}
			if (!(sc.state == SCE_PHP_WORD || sc.ch == '\\' || kwType == KeywordType::Constant)) {
				kwType = KeywordType::None;
			}
		}
	}

	sc.SetState(SCE_PHP_DEFAULT);
	return false;
}

// https://www.php.net/manual/en/function.sprintf.php
constexpr bool IsFormatSpecifier(char ch) noexcept {
	return AnyOf(ch, 'b',
					'c',
					'd',
					'e', 'E',
					'f', 'F',
					'g', 'G',
					'h', 'H',
					'o',
					's',
					'u',
					'x', 'X');
}

inline Sci_Position CheckFormatSpecifier(const StyleContext &sc, LexAccessor &styler, bool insideUrl) noexcept {
	if (sc.chNext == '%') {
		return 2;
	}
	if (insideUrl && IsHexDigit(sc.chNext)) {
		// percent encoded URL string
		return 0;
	}
	if (IsASpaceOrTab(sc.chNext) && IsADigit(sc.chPrev)) {
		// ignore word after percent: "5% x"
		return 0;
	}

	Sci_PositionU pos = sc.currentPos + 1;
	char ch = styler[pos];
	// argnum$
	while (IsADigit(ch)) {
		ch = styler[++pos];
	}
	if (ch == '$') {
		ch = styler[++pos];
	}
	// flags
	while (AnyOf(ch, ' ', '+', '-', '0')) {
		ch = styler[++pos];
	}
	if (ch == '\'') {
		if (sc.state == SCE_PHP_STRING_SQ) {
			return 0;
		}
		ch = styler[++pos]; // pad character
		if (static_cast<signed char>(ch) < ' ' || (ch == '\"' && sc.state == SCE_PHP_STRING_DQ)) {
			return 0;
		}
		ch = styler[++pos];
		while (AnyOf(ch, ' ', '+', '-', '0')) {
			ch = styler[++pos];
		}
	}
	// width
	while (IsADigit(ch)) {
		ch = styler[++pos];
	}
	// .precision
	if (ch == '.') {
		ch = styler[++pos];
		while (IsADigit(ch)) {
			ch = styler[++pos];
		}
	}
	// specifier
	if (IsFormatSpecifier(ch)) {
		return pos - sc.currentPos + 1;
	}
	return 0;
}

bool PHPLexer::HighlightInnerString() {
	if (sc.ch == '\\') {
		bool handled = false;
		if (sc.state == SCE_PHP_STRING_DQ || sc.state == SCE_PHP_HEREDOC) {
			if (!IsEOLChar(sc.chNext)) {
				handled = true;
				escSeq.resetEscapeState(sc.state, sc.chNext);
			}
		} else if (sc.state != SCE_PHP_NOWDOC) {
			if (sc.chNext == '\\' || sc.chNext == GetPHPStringQuote(sc.state)) {
				handled = true;
				escSeq.outerState = sc.state;
				escSeq.digitsLeft = 1;
			}
		}
		if (handled) {
			sc.SetState(SCE_PHP_ESCAPECHAR);
			sc.Forward();
			if (sc.Match('u', '{')) {
				escSeq.brace = true;
				escSeq.digitsLeft = 9; // Unicode code point
				sc.Forward();
			}
		}
	} else if (sc.ch == '%') {
		if (sc.state != SCE_PHP_STRING_BT) {
			const Sci_Position length = CheckFormatSpecifier(sc, sc.styler, insideUrl);
			if (length != 0) {
				const int outer = sc.state;
				sc.SetState(SCE_PHP_FORMAT_SPECIFIER);
				sc.Advance(length);
				sc.SetState(outer);
				return true;
			}
		}
	} else if (sc.ch == '$') {
		// ${} was deprecated since PHP 8.2 and removed in PHP 9.0
		// see https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation
		if (ExpandVariable(sc.state) && IsIdentifierStartEx(sc.chNext)) {
			insideUrl = false;
			SaveOuterStyle(sc.state);
			EnterExpansion(VariableType::Simple, 0);
			sc.SetState(SCE_PHP_VARIABLE2);
		}
	} else if (sc.Match('{', '$')) {
		insideUrl = false;
		if (ExpandVariable(sc.state)) {
			SaveOuterStyle(sc.state);
			EnterExpansion(VariableType::Complex, 1);
			sc.SetState(SCE_PHP_OPERATOR2);
		}
	} else if (sc.Match(':', '/', '/') && IsLowerCase(sc.chPrev)) {
		insideUrl = true;
	} else if (insideUrl && IsInvalidUrlChar(sc.ch)) {
		insideUrl = false;
	}
	return false;
}

bool PHPLexer::HighlightOperator(HtmlTextBlock block, int stylePrevNonWhite) {
	kwType = KeywordType::None;
	if (block == HtmlTextBlock::PHP) {
		const VariableType variableType = GetVariableType();
		sc.SetState((variableType == VariableType::Normal) ? SCE_PHP_OPERATOR : SCE_PHP_OPERATOR2);
		if (sc.ch == ']') {
			if (lineStateAttribute) {
				lineStateAttribute = 0;
			} else if (variableType == VariableType::Simple) {
				ExitExpansion();
				sc.ForwardSetState(TakeOuterStyle());
				return true;
			}
		} else if (variableType == VariableType::Complex) {
			if (AnyOf<'{', '}'>(sc.ch)) {
				VariableExpansion &expansion = nestedExpansion.back();
				expansion.braceCount += ('{' + '}')/2 - sc.ch;
				if (expansion.braceCount == 0) {
					ExitExpansion();
					sc.ForwardSetState(TakeOuterStyle());
					return true;
				}
			}
		}
	} else if (block == HtmlTextBlock::Script) {
		sc.SetState(js_style(SCE_JS_OPERATOR));
		if (!nestedState.empty() && nestedState.back() > SCE_PHP_LABEL) {
			sc.ChangeState(js_style(SCE_JS_OPERATOR2));
			if (sc.ch == '{') {
				SaveOuterStyle(js_style(SCE_JS_DEFAULT));
			} else if (sc.ch == '}') {
				const int outerState = TakeOuterStyle();
				sc.ForwardSetState(outerState);
				return true;
			}
		}
	} else if (block == HtmlTextBlock::Style) {
		sc.SetState(css_style(SCE_CSS_OPERATOR));
		if (AnyOf<'{', '}'>(sc.ch)) {
			propertyValue = 0;
			lineStateAttribute = 0;
			parenCount = 0;
			selectorLevel = 0;
		} else if (AnyOf<'[', ']'>(sc.ch)) {
			lineStateAttribute = (sc.ch & 4) ? 0 : LineStateAttributeLine;
		} else if (sc.ch == '(') {
			++parenCount;
		} else if (sc.ch == ')') {
			if (parenCount > 0) {
				--parenCount;
			}
			if (selectorLevel > 0) {
				selectorLevel--;
			}
		} else if (AnyOf<':', ';'>(sc.ch) && parenCount == 0) {
			if (sc.ch == ':') {
				if (!IsCssProperty(stylePrevNonWhite)) {
					propertyValue = CssLineStatePropertyValue;
				}
			} else {
				if (lineStateAttribute == 0) {
					propertyValue = 0;
				}
			}
		}
	}
	return false;
}

constexpr bool FollowExpression(int chPrevNonWhite, int stylePrevNonWhite) noexcept {
	return chPrevNonWhite == ')' || chPrevNonWhite == ']'
		|| (stylePrevNonWhite >= js_style(SCE_JS_NUMBER) && stylePrevNonWhite <= js_style(SCE_JS_OPERATOR_PF))
		|| IsJsIdentifierChar(chPrevNonWhite);
}

constexpr bool IsRegexStart(int chPrevNonWhite, int stylePrevNonWhite) noexcept {
	return stylePrevNonWhite < js_style(SCE_JS_DEFAULT)
		|| stylePrevNonWhite == js_style(SCE_JS_WORD)
		|| !FollowExpression(chPrevNonWhite, stylePrevNonWhite);
}

constexpr bool IsSpaceEquiv(int state) noexcept {
	return (state >= SCE_PHP_DEFAULT && state <= SCE_PHP_TASKMARKER)
		|| IsJsSpaceEquiv(state);
}

constexpr int GetDefaultStyle(int state) noexcept {
	return (state < SCE_PHP_LABEL) ? SCE_PHP_DEFAULT : js_style(SCE_JS_DEFAULT);
}

constexpr int GetTaskMarkerStyle(int state) noexcept {
	return (state < SCE_PHP_LABEL) ? SCE_PHP_TASKMARKER : js_style(SCE_JS_TASKMARKER);
}

constexpr int GetCommentTagStyle(int state) noexcept {
	return (state < SCE_PHP_LABEL) ? SCE_PHP_COMMENTTAGAT : js_style(SCE_JS_COMMENTTAGAT);
}

int PHPLexer::ClassifyJSWord(LexerWordList keywordLists) {
	char s[16];
	sc.GetCurrent(s, sizeof(s));
	if (keywordLists[KeywordIndex_JavaScript].InList(s)) {
		sc.ChangeState(js_style(SCE_JS_WORD));
		kwType = KeywordType::None;
		if (StrEqual(s, "function")) {
			const int chNext = sc.GetLineNextChar();
			if (IsJsIdentifierStart(chNext) || chNext == '\\') {
				kwType = KeywordType::Function;
			}
		}
	} else if (sc.ch == ':') {
		if (chBefore == ',' || chBefore == '{') {
			sc.ChangeState(js_style(SCE_JS_KEY));
		} else if (IsJumpLabelPrevASI(chBefore)) {
			sc.ChangeState(js_style(SCE_JS_LABEL));
		}
	} else {
		const int chNext = sc.GetDocNextChar();
		if (chNext == '(') {
			sc.ChangeState((kwType != KeywordType::None) ? js_style(SCE_JS_FUNCTION_DEFINITION) : js_style(SCE_JS_FUNCTION));
		}
	}

	const int state = sc.state;
	if (state != js_style(SCE_JS_WORD)) {
		kwType = KeywordType::None;
	}
	sc.SetState(js_style(SCE_JS_DEFAULT));
	return state;
}

void PHPLexer::HighlightJsInnerString() {
	if (sc.atLineStart) {
		if (lineContinuation) {
			lineContinuation = 0;
		} else {
			sc.SetState(js_style(SCE_JS_DEFAULT));
			return;
		}
	}
	if (sc.ch == '\\') {
		if (IsEOLChar(sc.chNext)) {
			lineContinuation = JsLineStateLineContinuation;
		} else {
			escSeq.resetEscapeState(sc.state, sc.chNext);
			sc.SetState(js_style(SCE_JS_ESCAPECHAR));
			sc.Forward();
			if (sc.Match('u', '{')) {
				escSeq.brace = true;
				escSeq.digitsLeft = 9; // Unicode code point
				sc.Forward();
			}
		}
	} else if (sc.state == js_style(SCE_JS_STRING_BT)) {
		if (sc.Match('$', '{')) {
			SaveOuterStyle(sc.state);
			sc.SetState(js_style(SCE_JS_OPERATOR2));
			sc.Forward();
		} else if (sc.ch == '`') {
			sc.ForwardSetState(js_style(SCE_JS_DEFAULT));
		}
	} else {
		if (sc.ch == ((sc.state == js_style(SCE_JS_STRING_SQ) ? '\'' : '\"'))) {
			sc.Forward();
			if (chBefore == ',' || chBefore == '{') {
				// json key
				const int chNext = sc.GetLineNextChar();
				if (chNext == ':') {
					sc.ChangeState(js_style(SCE_JS_KEY));
				}
			}
			sc.SetState(js_style(SCE_JS_DEFAULT));
		}
	}
}

bool PHPLexer::ClassifyCssWord() {
	char s[16];
	sc.GetCurrentLowered(s, sizeof(s));
	if (sc.state == css_style(SCE_CSS_PSEUDOCLASS)) {
		if (sc.ch == '(' && StrEqualsAny(s + 1, "is", "has", "not", "where", "current")) {
			++selectorLevel;
		}
		return false;
	}

	const int chNext = sc.GetDocNextChar(sc.ch == '(');
	if (sc.ch == '(') {
		sc.ChangeState(css_style(SCE_CSS_FUNCTION));
		if (StrEqual(s, "url") && !AnyOf(chNext, '\'', '\"', ')')) {
			parenCount++;
			sc.SetState(css_style(SCE_CSS_OPERATOR));
			sc.ForwardSetState(css_style(SCE_CSS_URL));
			return true;
		}
	} else if (chBefore == '!' && StrEqual(s, "important")) {
		sc.ChangeState(css_style(SCE_CSS_IMPORTANT));
	} else if (chNext == ':' && parenCount != 0) {
		// (descriptor: value)
		sc.ChangeState(css_style(SCE_CSS_PROPERTY));
	} else if (chBefore == ':' || chBefore == '=' || (parenCount == 0 && propertyValue)) {
		// [attribute = value]
		sc.ChangeState(css_style(SCE_CSS_VALUE));
	} else if (!propertyValue) {
		if (lineStateAttribute) {
			sc.ChangeState(css_style(SCE_CSS_ATTRIBUTE));
		} else if (chBefore == '.') {
			sc.ChangeState(css_style(SCE_CSS_CLASS));
		} else if (chBefore == '#') {
			sc.ChangeState(css_style(SCE_CSS_ID));
		} else if (chNext == ':' && (chBefore == ';' || chBefore == '{')) {
			// {property: value;}
			propertyValue = CssLineStatePropertyValue;
			sc.ChangeState(css_style(SCE_CSS_PROPERTY));
		} else if (parenCount == selectorLevel && !(chNext == '(')) {
			sc.ChangeState(css_style(SCE_CSS_TAG));
		}
	}
	return false;
}

void ColourisePHPDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int initStyle, LexerWordList keywordLists, Accessor &styler) {
	if (startPos != 0) {
		BacktrackToStart(styler, LineStateNestedStateLine, startPos, lengthDoc, initStyle);
	}

	PHPLexer lexer(startPos, lengthDoc, initStyle, styler);
	bool insideRegexRange = false;
	StyleContext &sc = lexer.sc;
	EscapeSequence &escSeq = lexer.escSeq;

	int visibleChars = 0;
	int visibleCharsBefore = 0;
	int chPrevNonWhite = 0;
	int stylePrevNonWhite = SCE_H_DEFAULT;
	DocTagState docTagState = DocTagState::None;
	int hereDocStartChar = 0;

	if (sc.currentLine > 0) {
		const uint32_t lineState = styler.GetLineState(sc.currentLine - 1);
		/*
		3: lineStateLineType
		1: lineStateAttribute
		1: lineContinuation
		1: propertyValue
		2: unused
		8: parenCount
		8: selectorLevel
		*/
		lexer.lineStateAttribute = lineState & LineStateAttributeLine;
		lexer.lineContinuation = lineState & JsLineStateLineContinuation;
		lexer.propertyValue = lineState & CssLineStatePropertyValue;
		lexer.parenCount = (lineState >> 8) & 0xff;
		lexer.selectorLevel = lineState >> 16;
	}
	if (startPos == 0) {
		if (sc.Match('#', '!')) {
			// Shell Shebang at beginning of file
			sc.SetState(SCE_PHP_COMMENTLINE);
			sc.Forward();
		}
	} else {
		unsigned marker = SCE_H_DEFAULT;
		// look back for better regex colouring
		if (IsJsSpaceEquiv(initStyle)) {
			marker = (js_style(SCE_JS_DEFAULT) << 8) | js_style(SCE_JS_TASKMARKER);
		} else if (IsCssSpaceEquiv(initStyle)) {
			marker = (css_style(SCE_CSS_DEFAULT) << 8) | css_style(SCE_CSS_CDO_CDC);
		} else if (IsSpaceEquiv(initStyle)) {
			marker = (SCE_PHP_DEFAULT << 8) | SCE_PHP_TASKMARKER;
		}
		if (marker != SCE_H_DEFAULT) {
			LookbackNonWhite(styler, startPos, marker, chPrevNonWhite, stylePrevNonWhite);
		}
	}

	while (sc.More()) {
		if ((sc.state < SCE_PHP_DEFAULT || sc.state > SCE_PHP_LABEL) && sc.Match('<', '?')) {
			lexer.HandlePHPTag();
		}

		switch (sc.state) {
		// PHP
		case SCE_PHP_OPERATOR:
		case SCE_PHP_OPERATOR2:
			sc.SetState(SCE_PHP_DEFAULT);
			break;

		case SCE_PHP_NUMBER:
		case js_style(SCE_JS_NUMBER):
		case css_style(SCE_CSS_NUMBER):
			if (!IsDecimalNumberEx(sc.chPrev, sc.ch, sc.chNext)) {
				if (sc.state == css_style(SCE_CSS_NUMBER)) {
					if (sc.ch == '%') {
						sc.Forward();
					} else if (IsCssIdentifierStart(sc.ch, sc.chNext)) {
						sc.ChangeState(css_style(SCE_CSS_DIMENSION));
						break;
					}
				}
				sc.SetState(escSeq.outerState);
			}
			break;

		case SCE_PHP_IDENTIFIER:
		case SCE_PHP_VARIABLE:
		case SCE_PHP_VARIABLE2:
		case SCE_PHP_HEREDOC_ID:
		case SCE_PHP_NOWDOC_ID:
			if (!IsIdentifierCharEx(sc.ch)) {
				if (lexer.ClassifyPHPWord(keywordLists, visibleChars)) {
					continue;
				}
			}
			break;

		case SCE_PHP_STRING_SQ:
		case SCE_PHP_STRING_DQ:
		case SCE_PHP_STRING_BT:
			if (sc.ch == GetPHPStringQuote(sc.state)) {
				sc.ForwardSetState(SCE_PHP_DEFAULT);
			} else if (lexer.HighlightInnerString()) {
				continue;
			}
			break;

		case SCE_PHP_ESCAPECHAR:
		case js_style(SCE_JS_ESCAPECHAR):
		case css_style(SCE_CSS_ESCAPECHAR):
			if (escSeq.atEscapeEnd(sc.ch)) {
				if (escSeq.brace && sc.ch == '}') {
					sc.Forward();
				}
				sc.SetState(escSeq.outerState);
				continue;
			}
			break;

		case SCE_PHP_HEREDOC:
		case SCE_PHP_NOWDOC:
			if (visibleChars == 0 && sc.ch == hereDocStartChar && lexer.IsHereDocEnd()) {
				sc.Forward(lexer.hereDocId.length());
				sc.SetState(SCE_PHP_DEFAULT);
			} else if (lexer.HighlightInnerString()) {
				continue;
			}
			break;

		case SCE_PHP_COMMENTLINE:
		case js_style(SCE_JS_COMMENTLINE):
			if (sc.state == SCE_PHP_COMMENTLINE && sc.Match('?', '>')) {
				lexer.HandleBlockEnd(HtmlTextBlock::PHP);
				continue;
			}
			if (sc.state != SCE_PHP_COMMENTLINE && sc.Match('<', '/') && lexer.HandleBlockEnd(HtmlTextBlock::Script)) {
				continue;// nop
			}
			if (sc.atLineStart) {
				sc.SetState(GetDefaultStyle(sc.state));
			} else {
				HighlightTaskMarker(sc, visibleChars, visibleCharsBefore, GetTaskMarkerStyle(sc.state));
			}
			break;

		case SCE_PHP_COMMENTBLOCK:
		case js_style(SCE_JS_COMMENTBLOCK):
			if (sc.Match('*', '/')) {
				sc.Forward();
				sc.ForwardSetState(GetDefaultStyle(sc.state));
			} else if (HighlightTaskMarker(sc, visibleChars, visibleCharsBefore, GetTaskMarkerStyle(sc.state))) {
				continue;
			}
			break;

		case SCE_PHP_COMMENTBLOCKDOC:
		case js_style(SCE_JS_COMMENTBLOCKDOC):
			switch (docTagState) {
			case DocTagState::At:
				docTagState = DocTagState::None;
				break;
			case DocTagState::InlineAt:
				if (sc.ch == '}') {
					docTagState = DocTagState::None;
					sc.SetState(GetCommentTagStyle(sc.state));
					sc.ForwardSetState(escSeq.outerState);
				}
				break;
			default:
				break;
			}
			if (sc.Match('*', '/')) {
				sc.Forward();
				sc.ForwardSetState(GetDefaultStyle(sc.state));
			} else if (sc.ch == '@' && IsAlpha(sc.chNext) && IsCommentTagPrev(sc.chPrev)) {
				docTagState = DocTagState::At;
				escSeq.outerState = sc.state;
				sc.SetState(GetCommentTagStyle(sc.state));
			} else if (sc.Match('{', '@') && IsAlpha(sc.GetRelative(2))) {
				docTagState = DocTagState::InlineAt;
				escSeq.outerState = sc.state;
				sc.SetState(GetCommentTagStyle(sc.state));
				sc.Forward();
			} else if (HighlightTaskMarker(sc, visibleChars, visibleCharsBefore, GetTaskMarkerStyle(sc.state))) {
				continue;
			}
			break;

		case SCE_PHP_COMMENTTAGAT:
		case js_style(SCE_JS_COMMENTTAGAT):
			if (!(IsIdentifierChar(sc.ch) || sc.ch == '-')) {
				sc.SetState(escSeq.outerState);
				continue;
			}
			break;

		// basic html
		case SCE_H_TAG:
		case SCE_H_QUESTION:
			if (sc.ch == '>' || sc.Match('/', '>') || IsASpace(sc.ch)
				|| (lexer.tagType == HtmlTagType::Question && sc.Match('?', '>'))) {
				lexer.ClassifyHtmlTag();
			}
			break;

		case SCE_H_ENTITY:
			if (!IsAlphaNumeric(sc.ch)) {
				if (sc.ch == ';') {
					sc.Forward();
				} else {
					sc.ChangeState(SCE_H_TAGUNKNOWN);
				}
				sc.SetState(SCE_H_DEFAULT);
			}
			break;

		case SCE_H_ATTRIBUTE:
			if (!IsHtmlAttrChar(sc.ch)) {
				sc.SetState(SCE_H_OTHER);
				continue;
			}
			break;

		case SCE_H_VALUE:
		case SCE_H_SGML_1ST_PARAM:
			if (IsHtmlInvalidAttrChar(sc.ch)) {
				const int outer = (sc.state == SCE_H_VALUE) ? SCE_H_OTHER: SCE_H_SGML_DEFAULT;
				sc.SetState(outer);
				continue;
			}
			break;

		case SCE_H_SINGLESTRING:
		case SCE_H_SGML_SIMPLESTRING:
			if (sc.ch == '\'') {
				const int outer = (sc.state == SCE_H_SINGLESTRING) ? SCE_H_OTHER : SCE_H_SGML_DEFAULT;
				sc.ForwardSetState(outer);
				continue;
			}
			break;

		case SCE_H_DOUBLESTRING:
		case SCE_H_SGML_DOUBLESTRING:
			if (sc.ch == '\"') {
				const int outer = (sc.state == SCE_H_DOUBLESTRING) ? SCE_H_OTHER : SCE_H_SGML_DEFAULT;
				sc.ForwardSetState(outer);
				continue;
			}
			break;

		case SCE_H_OTHER:
			if (sc.ch == '>' || sc.Match('/', '>') || (lexer.tagType == HtmlTagType::Question && sc.Match('?', '>'))) {
				lexer.ClassifyHtmlTag();
				break;
			}
			if (sc.ch == '<') {
				// html tag on typing
				sc.SetState(SCE_H_DEFAULT);
				break;
			}
			if (sc.ch == '\'') {
				sc.SetState(SCE_H_SINGLESTRING);
			} else if (sc.ch == '\"') {
				sc.SetState(SCE_H_DOUBLESTRING);
			} else if (IsHtmlAttrStart(sc.ch)) {
				sc.SetState(SCE_H_ATTRIBUTE);
			} else if (!IsHtmlInvalidAttrChar(sc.ch)) {
				sc.SetState(SCE_H_VALUE);
			}
			break;

		case SCE_H_COMMENT:
			if (sc.Match('-', '-')) {
				do {
					sc.Forward();
				} while (sc.ch == '-');
				// close HTML comment with --!>
				// https://html.spec.whatwg.org/multipage/parsing.html#parse-error-incorrectly-closed-comment
				if (sc.ch == '>' || sc.Match('!', '>')) {
					sc.Forward((sc.ch == '>') ? 1 : 2);
					sc.SetState(SCE_H_DEFAULT);
				}
			}
			break;

		case SCE_H_CDATA:
			if (sc.Match(']', ']', '>')) {
				sc.Advance(3);
				sc.SetState(SCE_H_DEFAULT);
			}
			break;

		case SCE_H_SGML_COMMENT:
			if (sc.Match('-', '-')) {
				sc.Forward();
				sc.ForwardSetState(SCE_H_SGML_DEFAULT);
				continue;
			}
			break;

		case SCE_H_SGML_COMMAND:
		case SCE_H_SGML_DEFAULT:
			if (sc.ch == '>') {
				if (sc.state == SCE_H_SGML_DEFAULT) {
					sc.SetState(SCE_H_SGML_COMMAND);
				}
				sc.ForwardSetState(SCE_H_DEFAULT);
			} else if (sc.state == SCE_H_SGML_COMMAND) {
				if (IsASpace(sc.ch)) {
					sc.SetState(SCE_H_SGML_DEFAULT);
				}
			} else {
				if (sc.ch == '\"') {
					sc.SetState(SCE_H_SGML_DOUBLESTRING);
				} else if (sc.ch == '\'') {
					sc.SetState(SCE_H_SGML_SIMPLESTRING);
				} else if (sc.Match('-', '-')) {
					sc.SetState(SCE_H_SGML_COMMENT);
					sc.Forward();
				} else if (IsASpace(sc.chPrev)) {
					if (IsUpperCase(sc.ch) || (sc.ch == '#' && IsUpperCase(sc.chNext))) {
						sc.SetState(SCE_H_SGML_COMMAND);
					} else if (!IsHtmlInvalidAttrChar(sc.ch)) {
						sc.SetState(SCE_H_SGML_1ST_PARAM);
					}
				}
			}
			break;

		// basic JavaScript
		case js_style(SCE_JS_OPERATOR):
		case js_style(SCE_JS_OPERATOR2):
		case js_style(SCE_JS_OPERATOR_PF):
			sc.SetState(js_style(SCE_JS_DEFAULT));
			break;

		case js_style(SCE_JS_IDENTIFIER):
			if (sc.Match('\\', 'u')) {
				sc.Forward();
			} else if (!IsJsIdentifierChar(sc.ch)) {
				stylePrevNonWhite = lexer.ClassifyJSWord(keywordLists);
			}
			break;

		case js_style(SCE_JS_STRING_SQ):
		case js_style(SCE_JS_STRING_DQ):
		case js_style(SCE_JS_STRING_BT):
			lexer.HighlightJsInnerString();
			break;

		case js_style(SCE_JS_REGEX):
			if (sc.atLineStart) {
				sc.SetState(js_style(SCE_JS_DEFAULT));
			} else if (sc.ch == '\\') {
				sc.Forward();
			} else if (sc.ch == '[' || sc.ch == ']') {
				insideRegexRange = sc.ch == '[';
			} else if (sc.ch == '/' && !insideRegexRange) {
				sc.Forward();
				// regex flags
				while (IsLowerCase(sc.ch)) {
					sc.Forward();
				}
				sc.SetState(js_style(SCE_JS_DEFAULT));
			}
			break;

		// basic CSS
		case css_style(SCE_CSS_OPERATOR):
		case css_style(SCE_CSS_CDO_CDC):
			sc.SetState(css_style(SCE_CSS_DEFAULT));
			break;

		case css_style(SCE_CSS_COMMENTBLOCK):
			if (sc.Match('*', '/')) {
				sc.Forward();
				sc.ForwardSetState(css_style(SCE_CSS_DEFAULT));
			}
			break;

		case css_style(SCE_CSS_DIMENSION):
		case css_style(SCE_CSS_AT_RULE):
		case css_style(SCE_CSS_IDENTIFIER):
		case css_style(SCE_CSS_PSEUDOCLASS):
		case css_style(SCE_CSS_PSEUDOELEMENT):
			if (!IsCssIdentifierChar(sc.ch)) {
				if (AnyOf(sc.state, css_style(SCE_CSS_IDENTIFIER), css_style(SCE_CSS_PSEUDOCLASS)) && lexer.ClassifyCssWord()) {
					continue;
				}

				stylePrevNonWhite = sc.state;
				sc.SetState(css_style(SCE_CSS_DEFAULT));
			}
			break;

		case css_style(SCE_CSS_STRING_SQ):
		case css_style(SCE_CSS_STRING_DQ):
		case css_style(SCE_CSS_URL):
			if (sc.ch == '\\') {
				if (!IsEOLChar(sc.chNext)) {
					escSeq.outerState = sc.state;
					escSeq.hex = true;
					escSeq.digitsLeft = IsHexDigit(sc.chNext) ? 6 : 1;
					sc.SetState(css_style(SCE_CSS_ESCAPECHAR));
					sc.Forward();
				}
			} else if (sc.ch == ')' && sc.state == css_style(SCE_CSS_URL)) {
				sc.SetState(css_style(SCE_CSS_DEFAULT));
			} else if ((sc.ch == '\'' && sc.state == css_style(SCE_CSS_STRING_SQ))
				|| (sc.ch == '\"' && sc.state == css_style(SCE_CSS_STRING_DQ))) {
				sc.ForwardSetState(css_style(SCE_CSS_DEFAULT));
			}
			break;

		case css_style(SCE_CSS_UNICODE_RANGE):
			if (sc.ch == '-' && IsCssUnicodeRangeChar(sc.chNext)) {
				escSeq.digitsLeft = 7;
			} else if (escSeq.atUnicodeRangeEnd(sc.ch)) {
				sc.SetState(css_style(SCE_CSS_DEFAULT));
			}
			break;
		}

		switch (sc.state) {
		case SCE_H_DEFAULT:
			if (sc.ch == '<') {
				const int chNext = sc.GetRelative(2);
				if (sc.chNext == '!') {
					if (chNext == '-' && sc.GetRelative(3) == '-') {
						sc.SetState(SCE_H_COMMENT);
						sc.Advance(3);
						// handle empty comment: <!-->, <!--->
						// https://html.spec.whatwg.org/multipage/parsing.html#parse-error-abrupt-closing-of-empty-comment
						if (sc.chNext == '>' || sc.MatchNext('-', '>')) {
							sc.Forward((sc.chNext == '>') ? 2 : 3);
							sc.SetState(SCE_H_DEFAULT);
							continue;
						}
					} else if (chNext == '[' && styler.Match(sc.currentPos + 3, "CDATA[")) {
						// <![CDATA[ ]]>
						sc.SetState(SCE_H_CDATA);
						sc.Advance(8);
					} else if (IsAlpha(chNext)) {
						// <!DOCTYPE html>
						sc.SetState(SCE_H_SGML_COMMAND);
					}
				} else if (IsHtmlTagStart(sc.chNext) || (sc.chNext == '/' && IsHtmlTagStart(chNext))) {
					lexer.tagType = HtmlTagType::None;
					sc.SetState(SCE_H_TAG);
					if (sc.chNext == '/') {
						sc.Forward();
					}
				}
			} else if (sc.ch == '&') {
				if (IsAlpha(sc.chNext) || sc.chNext == '#') {
					sc.SetState(SCE_H_ENTITY);
					if (sc.chNext == '#') {
						sc.Forward();
					}
				}
			}
			break;

		case SCE_PHP_DEFAULT:
			if (sc.Match('?', '>')) {
				lexer.HandleBlockEnd(HtmlTextBlock::PHP);
				continue;
			}
			if (sc.Match('#', '[')) {
				lexer.lineStateAttribute = LineStateAttributeLine;
				sc.SetState(SCE_PHP_ATTRIBUTE);
				sc.ForwardSetState(SCE_PHP_OPERATOR);
			} else if (sc.ch == '#' || sc.Match('/', '/')) {
				visibleCharsBefore = visibleChars;
				if (visibleChars == 0) {
					lexer.lineStateLineType = LineStateCommentLine;
				}
				sc.SetState(SCE_PHP_COMMENTLINE);
			} else if (sc.Match('/', '*')) {
				visibleCharsBefore = visibleChars;
				docTagState = DocTagState::None;
				sc.SetState(SCE_PHP_COMMENTBLOCK);
				sc.Forward(2);
				if (sc.ch == '*' && sc.chNext != '*') {
					sc.ChangeState(SCE_PHP_COMMENTBLOCKDOC);
				}
				continue;
			} else if (sc.ch == '\'') {
				lexer.insideUrl = false;
				sc.SetState(SCE_PHP_STRING_SQ);
			} else if (sc.ch == '\"') {
				lexer.insideUrl = false;
				sc.SetState(SCE_PHP_STRING_DQ);
			} else if (sc.ch == '`') {
				sc.SetState(SCE_PHP_STRING_BT);
			} else if (sc.Match('<', '<')) {
				if (sc.GetRelative(2) == '<') {
					const int ch = sc.GetRelative(3);
					if (ch == '\'') {
						sc.SetState(SCE_PHP_NOWDOC);
						sc.Advance(4);
						hereDocStartChar = sc.ch;
						sc.SetState(SCE_PHP_NOWDOC_ID);
					} else if (ch == '\"' || IsIdentifierStartEx(ch)) {
						sc.SetState(SCE_PHP_HEREDOC);
						sc.Advance(3 + (ch == '\"'));
						hereDocStartChar = sc.ch;
						sc.SetState(SCE_PHP_HEREDOC_ID);
					}
				}
				if (sc.state == SCE_PHP_DEFAULT) {
					sc.SetState(SCE_PHP_OPERATOR);
				}
			} else if (sc.ch == '$') {
				if (IsIdentifierStartEx(sc.chNext)) {
					sc.SetState(SCE_PHP_VARIABLE);
				} else {
					sc.SetState(SCE_PHP_OPERATOR);
				}
			} else if (IsADigit(sc.ch)) {
				escSeq.outerState = SCE_PHP_DEFAULT;
				sc.SetState(SCE_PHP_NUMBER);
			} else if (IsIdentifierStartEx(sc.ch)) {
				lexer.chBefore = chPrevNonWhite;
				sc.SetState(SCE_PHP_IDENTIFIER);
			} else if (IsAGraphic(sc.ch)) {
				if (lexer.HighlightOperator(HtmlTextBlock::PHP, stylePrevNonWhite)) {
					continue;
				}
			}
			break;

		case js_style(SCE_JS_DEFAULT):
			if (sc.Match('<', '/') && lexer.HandleBlockEnd(HtmlTextBlock::Script)) {
				// nop
			} else if (sc.ch == '/') {
				if (sc.chNext == '/' || sc.chNext == '*') {
					visibleCharsBefore = visibleChars;
					docTagState = DocTagState::None;
					const int chNext = sc.chNext;
					sc.SetState((chNext == '*') ? js_style(SCE_JS_COMMENTBLOCK) : js_style(SCE_JS_COMMENTLINE));
					if (chNext== '*') {
						sc.Forward(2);
						if (sc.ch == '*' && sc.chNext != '*') {
							sc.ChangeState(js_style(SCE_JS_COMMENTBLOCKDOC));
						}
						continue;
					}
					if (visibleChars == 0) {
						lexer.lineStateLineType = LineStateCommentLine;
					}
				} else if (!IsEOLChar(sc.chNext) && IsRegexStart(chPrevNonWhite, stylePrevNonWhite)) {
					insideRegexRange = false;
					sc.SetState(js_style(SCE_JS_REGEX));
				} else {
					sc.SetState(js_style(SCE_JS_OPERATOR));
				}
			} else if (sc.ch == '\'' || sc.ch == '\"') {
				lexer.chBefore = chPrevNonWhite;
				sc.SetState((sc.ch == '\'') ? js_style(SCE_JS_STRING_SQ) : js_style(SCE_JS_STRING_DQ));
			} else if (sc.ch == '`') {
				sc.SetState(js_style(SCE_JS_STRING_BT));
			} else if (IsNumberStartEx(sc.chPrev, sc.ch, sc.chNext)) {
				escSeq.outerState = js_style(SCE_JS_DEFAULT);
				sc.SetState(js_style(SCE_JS_NUMBER));
			} else if (IsJsIdentifierStart(sc.ch) || sc.Match('\\', 'u')) {
				lexer.chBefore = chPrevNonWhite;
				sc.SetState(js_style(SCE_JS_IDENTIFIER));
			} else if (sc.ch == '+' || sc.ch == '-') {
				if (sc.ch == sc.chNext) {
					// highlight ++ and -- as different style to simplify regex detection.
					sc.SetState(js_style(SCE_JS_OPERATOR_PF));
					sc.Forward();
				} else {
					sc.SetState(js_style(SCE_JS_OPERATOR));
				}
			} else if (IsAGraphic(sc.ch) && sc.ch != '\\') {
				if (lexer.HighlightOperator(HtmlTextBlock::Script, stylePrevNonWhite)) {
					continue;
				}
			}
			break;

		case css_style(SCE_CSS_DEFAULT):
			if (sc.Match('<', '/') && lexer.HandleBlockEnd(HtmlTextBlock::Style)) {
				// nop
			} else if (sc.Match('/', '*')) {
				sc.SetState(css_style(SCE_CSS_COMMENTBLOCK));
				sc.Forward();
			} else if (sc.ch == '\'') {
				sc.SetState(css_style(SCE_CSS_STRING_SQ));
			} else if (sc.ch == '\"') {
				sc.SetState(css_style(SCE_CSS_STRING_DQ));
			} else if (IsHtmlCommentDelimiter(sc)) {
				sc.SetState(css_style(SCE_CSS_CDO_CDC));
				sc.Advance((sc.ch == '<') ? 3 : 2);
			} else if (IsNumberStart(sc.ch, sc.chNext)
				|| (sc.ch == '#' && (lexer.propertyValue || lexer.parenCount > lexer.selectorLevel) && IsHexDigit(sc.chNext))) {
				escSeq.outerState = css_style(SCE_CSS_DEFAULT);
				sc.SetState(css_style(SCE_CSS_NUMBER));
			} else if (sc.chNext == '+' && UnsafeLower(sc.ch) == 'u'
				&& lexer.propertyValue && (chPrevNonWhite == ':' || chPrevNonWhite == ',')
				&& IsCssUnicodeRangeChar(sc.GetRelative(2))) {
				escSeq.digitsLeft = 7;
				sc.SetState(css_style(SCE_CSS_UNICODE_RANGE));
				sc.Forward();
			} else if (IsCssIdentifierStart(sc.ch, sc.chNext)) {
				lexer.chBefore = chPrevNonWhite;
				sc.SetState((sc.ch == '@') ? css_style(SCE_CSS_AT_RULE) : css_style(SCE_CSS_IDENTIFIER));
			} else if (sc.Match(':', ':') && IsCssIdentifierNext(sc.GetRelative(2))) {
				sc.SetState(css_style(SCE_CSS_PSEUDOELEMENT));
				sc.Forward(2);
			} else if (sc.ch == ':' && !IsCssProperty(stylePrevNonWhite)
				&& IsCssIdentifierNext(sc.chNext)) {
				sc.SetState(css_style(SCE_CSS_PSEUDOCLASS));
				sc.Forward();
			} else if (IsAGraphic(sc.ch)) {
				lexer.HighlightOperator(HtmlTextBlock::Style, stylePrevNonWhite);
			}
			break;
		}

		if (!isspacechar(sc.ch)) {
			visibleChars++;
			if (!(IsJsSpaceEquiv(sc.state) || IsCssSpaceEquiv(sc.state))) {
				chPrevNonWhite = sc.ch;
				stylePrevNonWhite = sc.state;
			}
		}
		if (sc.atLineEnd) {
			styler.SetLineState(sc.currentLine, lexer.LineState());
			lexer.lineStateLineType = 0;
			lexer.kwType = KeywordType::None;
			visibleChars = 0;
			visibleCharsBefore = 0;
			docTagState = DocTagState::None;
		}
		sc.Forward();
	}

	sc.Complete();
}

struct FoldLineState {
	int lineComment;
	int useNamespace;
	constexpr explicit FoldLineState(int lineState) noexcept:
		lineComment(lineState & LineStateCommentLine),
		useNamespace((lineState >> 1) & 1) {
	}
};

void FoldPHPDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int initStyle, LexerWordList /*keywordLists*/, Accessor &styler) {
	const Sci_PositionU endPos = startPos + lengthDoc;
	Sci_Line lineCurrent = styler.GetLine(startPos);
	int levelCurrent = SC_FOLDLEVELBASE;
	FoldLineState foldPrev(0);
	if (lineCurrent > 0) {
		levelCurrent = styler.LevelAt(lineCurrent - 1) >> 16;
		foldPrev = FoldLineState(styler.GetLineState(lineCurrent - 1));
		Sci_PositionU bracePos = 0;
		const HtmlTextBlock block = GetHtmlTextBlock(initStyle);
		if (block == HtmlTextBlock::PHP) {
			bracePos = CheckBraceOnNextLine(styler, lineCurrent - 1, SCE_PHP_OPERATOR, SCE_PHP_TASKMARKER);
		} else if (block == HtmlTextBlock::Script) {
			bracePos = CheckBraceOnNextLine(styler, lineCurrent - 1, js_style(SCE_JS_OPERATOR), js_style(SCE_JS_TASKMARKER));
		}
		if (bracePos) {
			startPos = bracePos + 1; // skip the brace
		}
	}

	int levelNext = levelCurrent;
	FoldLineState foldCurrent(styler.GetLineState(lineCurrent));
	Sci_PositionU lineStartNext = styler.LineStart(lineCurrent + 1);
	lineStartNext = sci::min(lineStartNext, endPos);

	char chNext = styler[startPos];
	int styleNext = styler.StyleAt(startPos);
	int style = initStyle;
	int visibleChars = 0;

	while (startPos < endPos) {
		const char ch = chNext;
		const int stylePrev = style;
		style = styleNext;
		chNext = styler[++startPos];
		styleNext = styler.StyleAt(startPos);

		switch (style) {
		case SCE_PHP_COMMENTBLOCK:
		case SCE_PHP_COMMENTBLOCKDOC:
		case js_style(SCE_JS_COMMENTBLOCK):
		case js_style(SCE_JS_COMMENTBLOCKDOC):
		case css_style(SCE_CSS_COMMENTBLOCK):
		case SCE_H_COMMENT:
		case SCE_H_CDATA:
		case SCE_PHP_STRING_SQ:
		case SCE_PHP_STRING_BT:
		case SCE_PHP_STRING_DQ:
		case SCE_PHP_HEREDOC:
		case SCE_PHP_NOWDOC:
		case js_style(SCE_JS_STRING_BT):
			if (style != stylePrev) {
				levelNext++;
			}
			if (style != styleNext) {
				levelNext--;
			}
			break;

		case SCE_PHP_OPERATOR:
		case SCE_PHP_OPERATOR2:
		case js_style(SCE_JS_OPERATOR):
		case js_style(SCE_JS_OPERATOR2):
		case css_style(SCE_CSS_OPERATOR):
			if (ch == '{' || ch == '[' || ch == '(') {
				levelNext++;
			} else if (ch == '}' || ch == ']' || ch == ')') {
				levelNext--;
			}
			break;

		case SCE_H_TAG:
			if (ch == '<') {
				if (chNext == '/') {
					levelNext--;
				} else {
					levelNext++;
				}
			} else if (ch == '/' && chNext == '>') {
				levelNext--;
			}
			break;

		case SCE_H_QUESTION:
			if (ch == '<' && chNext == '?') {
				levelNext++;
			} else if (ch == '?' && chNext == '>') {
				levelNext--;
			}
			break;
		}

		if (visibleChars == 0 && !IsSpaceEquiv(style)) {
			++visibleChars;
		}
		if (startPos == lineStartNext) {
			const FoldLineState foldNext(styler.GetLineState(lineCurrent + 1));
			levelNext = sci::max(levelNext, SC_FOLDLEVELBASE);
			if (foldCurrent.lineComment) {
				levelNext += foldNext.lineComment - foldPrev.lineComment;
			} else if (foldCurrent.useNamespace) {
				levelNext += foldNext.useNamespace - foldPrev.useNamespace;
			} else if (visibleChars) {
				Sci_PositionU bracePos = 0;
				const HtmlTextBlock block = GetHtmlTextBlock(style);
				if (block == HtmlTextBlock::PHP) {
					bracePos = CheckBraceOnNextLine(styler, lineCurrent, SCE_PHP_OPERATOR, SCE_PHP_TASKMARKER);
				} else if (block == HtmlTextBlock::Script) {
					bracePos = CheckBraceOnNextLine(styler, lineCurrent, js_style(SCE_JS_OPERATOR), js_style(SCE_JS_TASKMARKER));
				}
				if (bracePos) {
					levelNext++;
					startPos = bracePos + 1; // skip the brace
					style = (block == HtmlTextBlock::PHP) ? SCE_PHP_OPERATOR : js_style(SCE_JS_OPERATOR);
					styleNext = styler.StyleAt(startPos);
				}
			}

			const int levelUse = levelCurrent;
			int lev = levelUse | (levelNext << 16);
			if (levelUse < levelNext) {
				lev |= SC_FOLDLEVELHEADERFLAG;
			}
			styler.SetLevel(lineCurrent, lev);

			lineCurrent++;
			lineStartNext = styler.LineStart(lineCurrent + 1);
			lineStartNext = sci::min(lineStartNext, endPos);
			levelCurrent = levelNext;
			foldPrev = foldCurrent;
			foldCurrent = foldNext;
			visibleChars = 0;
		}
	}
}

}

LexerModule lmPHPScript(SCLEX_PHPSCRIPT, ColourisePHPDoc, "php", FoldPHPDoc);
